#include "../CommonCommandFlags.h"
#include "ProjectHistory.h"
#include "SyncLock.h"
#include "TrackFocus.h"
#include "Viewport.h"
#include "UndoManager.h"
#include "WaveClip.h"
#include "WaveClipUtilities.h"
#include "ViewInfo.h"
#include "WaveTrack.h"
#include "CommandContext.h"
#include "MenuRegistry.h"
#include "../tracks/ui/TimeShiftHandle.h"

#include <cassert>

// private helper classes and functions
namespace {
struct FoundTrack {
    const WaveTrack* waveTrack{};
    int trackNum{};

    wxString ComposeTrackName() const
    {
        /* i18n-hint: The %d is replaced by the number of the track.*/
        auto shortName = wxString::Format(_("Track %d"), trackNum)
                         .Append(" " + waveTrack->GetName());
        return shortName;
    }
};

struct FoundClip : FoundTrack {
    bool found{};
    double startTime{};
    double endTime{};
    wxString name{};
    int index{};
};

struct FoundClipBoundary : FoundTrack {
    int nFound{};   // 0, 1, or 2
    double time{};
    int index1{};
    wxString name1{};
    bool clipStart1{};
    int index2{};
    wxString name2{};
    bool clipStart2{};
};

// When two clips are immediately next to each other, the GetPlayEndTime() of the
// first clip and the GetPlayStartTime() of the second clip may not be exactly equal
// due to rounding errors. When searching for the next/prev start time from a
// given time, the following function adjusts that given time if necessary to
// take this into account. If the given time is the end time of the first of two
// clips which are next to each other, then the given time is changed to the
// start time of the second clip. This ensures that the correct next/prev start
// time is found.
double AdjustForFindingStartTimes(
    const WaveTrack::IntervalConstHolders& clips, double time)
{
    auto q = std::find_if(clips.begin(), clips.end(),
                          [&] (const auto& clip) {
        return clip->GetPlayEndTime() == time;
    });
    if (q != clips.end() && q + 1 != clips.end()
        && WaveClipUtilities::SharesBoundaryWithNextClip(**q, **(q + 1))) {
        time = (*(q + 1))->GetPlayStartTime();
    }

    return time;
}

// When two clips are immediately next to each other, the GetPlayEndTime() of the
// first clip and the GetPlayStartTime() of the second clip may not be exactly equal
// due to rounding errors. When searching for the next/prev end time from a
// given time, the following function adjusts that given time if necessary to
// take this into account. If the given time is the start time of the second of
// two clips which are next to each other, then the given time is changed to the
// end time of the first clip. This ensures that the correct next/prev end time
// is found.
double AdjustForFindingEndTimes(
    const WaveTrack::IntervalConstHolders& clips, double time)
{
    auto q = std::find_if(clips.begin(), clips.end(),
                          [&] (auto& clip) {
        return clip->GetPlayStartTime() == time;
    });
    if (q != clips.end() && q != clips.begin()
        && WaveClipUtilities::SharesBoundaryWithNextClip(**(q - 1), **q)) {
        time = (*(q - 1))->GetPlayEndTime();
    }

    return time;
}

FoundClipBoundary FindNextClipBoundary(const WaveTrack* wt, double time)
{
    FoundClipBoundary result{};
    result.waveTrack = wt;
    const auto clips = wt->SortedIntervalArray();
    double timeStart = AdjustForFindingStartTimes(clips, time);
    double timeEnd = AdjustForFindingEndTimes(clips, time);

    auto pStart = std::find_if(clips.begin(), clips.end(),
                               [&] (const auto& clip) {
        return clip->GetPlayStartTime() > timeStart;
    });
    auto pEnd = std::find_if(clips.begin(), clips.end(),
                             [&] (const auto& clip) {
        return clip->GetPlayEndTime() > timeEnd;
    });

    if (pStart != clips.end() && pEnd != clips.end()) {
        if (WaveClipUtilities::SharesBoundaryWithNextClip(**pEnd, **pStart)) {
            // boundary between two clips which are immediately next to each other.
            result.nFound = 2;
            result.time = (*pEnd)->GetPlayEndTime();
            result.index1 = std::distance(clips.begin(), pEnd);
            result.name1 = (*pEnd)->GetName();
            result.clipStart1 = false;
            result.index2 = std::distance(clips.begin(), pStart);
            result.name2 = (*pStart)->GetName();
            result.clipStart2 = true;
        } else if ((*pStart)->GetPlayStartTime() < (*pEnd)->GetPlayEndTime()) {
            result.nFound = 1;
            result.time = (*pStart)->GetPlayStartTime();
            result.index1 = std::distance(clips.begin(), pStart);
            result.name1 = (*pStart)->GetName();
            result.clipStart1 = true;
        } else {
            result.nFound = 1;
            result.time = (*pEnd)->GetPlayEndTime();
            result.index1 = std::distance(clips.begin(), pEnd);
            result.name1 = (*pEnd)->GetName();
            result.clipStart1 = false;
        }
    } else if (pEnd != clips.end()) {
        result.nFound = 1;
        result.time = (*pEnd)->GetPlayEndTime();
        result.index1 = std::distance(clips.begin(), pEnd);
        result.name1 = (*pEnd)->GetName();
        result.clipStart1 = false;
    }

    return result;
}

FoundClipBoundary FindPrevClipBoundary(const WaveTrack* wt, double time)
{
    FoundClipBoundary result{};
    result.waveTrack = wt;
    const auto clips = wt->SortedIntervalArray();
    double timeStart = AdjustForFindingStartTimes(clips, time);
    double timeEnd = AdjustForFindingEndTimes(clips, time);

    auto pStart = std::find_if(clips.rbegin(), clips.rend(),
                               [&] (const auto& clip) {
        return clip->GetPlayStartTime() < timeStart;
    });
    auto pEnd = std::find_if(clips.rbegin(), clips.rend(),
                             [&] (const auto& clip) {
        return clip->GetPlayEndTime() < timeEnd;
    });

    if (pStart != clips.rend() && pEnd != clips.rend()) {
        if (WaveClipUtilities::SharesBoundaryWithNextClip(**pEnd, **pStart)) {
            // boundary between two clips which are immediately next to each other.
            result.nFound = 2;
            result.time = (*pStart)->GetPlayStartTime();
            result.index1
                =static_cast<int>(clips.size()) - 1
                  - std::distance(clips.rbegin(), pStart);
            result.name1 = (*pStart)->GetName();
            result.clipStart1 = true;
            result.index2
                =static_cast<int>(clips.size()) - 1
                  - std::distance(clips.rbegin(), pEnd);
            result.name2 = (*pEnd)->GetName();
            result.clipStart2 = false;
        } else if ((*pStart)->GetPlayStartTime() > (*pEnd)->GetPlayEndTime()) {
            result.nFound = 1;
            result.time = (*pStart)->GetPlayStartTime();
            result.index1
                =static_cast<int>(clips.size()) - 1
                  - std::distance(clips.rbegin(), pStart);
            result.name1 = (*pStart)->GetName();
            result.clipStart1 = true;
        } else {
            result.nFound = 1;
            result.time = (*pEnd)->GetPlayEndTime();
            result.index1
                =static_cast<int>(clips.size()) - 1
                  - std::distance(clips.rbegin(), pEnd);
            result.name1 = (*pEnd)->GetName();
            result.clipStart1 = false;
        }
    } else if (pStart != clips.rend()) {
        result.nFound = 1;
        result.time = (*pStart)->GetPlayStartTime();
        result.index1
            =static_cast<int>(clips.size()) - 1
              - std::distance(clips.rbegin(), pStart);
        result.name1 = (*pStart)->GetName();
        result.clipStart1 = true;
    }

    return result;
}

int FindClipBoundaries
    (AudacityProject& project,
    double time, bool next, std::vector<FoundClipBoundary>& finalResults)
{
    auto& tracks = TrackList::Get(project);
    finalResults.clear();

    bool anyWaveTracksSelected{ tracks.Selected<const WaveTrack>() };

    // first search the tracks individually

    std::vector<FoundClipBoundary> results;

    int nTracksSearched = 0;
    auto all = tracks.Any();
    auto waveTracks = all.Filter<const WaveTrack>();
    if (anyWaveTracksSelected) {
        waveTracks = waveTracks + &Track::GetSelected;
    }
    for (auto waveTrack : waveTracks) {
        auto result = next ? FindNextClipBoundary(waveTrack, time)
                      : FindPrevClipBoundary(waveTrack, time);
        if (result.nFound > 0) {
            result.trackNum
                =1 + std::distance(all.begin(), all.find(waveTrack));
            results.push_back(result);
        }

        nTracksSearched++;
    }

    if (results.size() > 0) {
        // If any clip boundaries were found
        // find the clip boundary or boundaries with the min/max time
        auto compare = [] (const FoundClipBoundary& a, const FoundClipBoundary& b)
        { return a.time < b.time; };

        auto p = next ? min_element(results.begin(), results.end(), compare)
                 : max_element(results.begin(), results.end(), compare);

        for ( auto& r : results ) {
            if (r.time == (*p).time) {
                finalResults.push_back(r);
            }
        }
    }

    return nTracksSearched; // can be used for screen reader messages if required
}

// for clip boundary commands, create a message for screen readers
TranslatableString ClipBoundaryMessage(
    const std::vector<FoundClipBoundary>& results)
{
    TranslatableString message;
    for (auto& result : results) {
        auto longName = result.ComposeTrackName();

        TranslatableString str;
        auto nClips = result.waveTrack->GetNumClips();
        if (result.nFound < 2) {
            str = XP(
                /* i18n-hint:
                   First %s is replaced with the noun "start" or "end"
                   identifying one end of a clip,
                   second string is the name of that clip,
                   first number gives the position of that clip in a sequence
                   of clips,
                   last number counts all clips,
                   and the last string is the name of the track containing the
                   clips.
                 */
                "%s %s, %d of %d clip %s",
                "%s %s, %d of %d clips %s",
                3
                )(
                result.clipStart1 ? XO("start") : XO("end"),
                result.name1,
                result.index1 + 1,
                nClips,
                longName
                );
        } else {
            str = XP(
                /* i18n-hint:
                   First and third %s are each replaced with the noun "start"
                   or with "end", identifying and end of a clip,
                   second and fourth strings are the names of those clips,
                   first and second numbers give the position of those clips in
                   a sequence of clips,
                   last number counts all clips,
                   and the last string is the name of the track containing the
                   clips.
                 */
                "%s %s and %s %s, %d and %d of %d clip %s",
                "%s %s and %s %s, %d and %d of %d clips %s",
                6
                )(
                result.clipStart1 ? XO("start") : XO("end"),
                result.name1,
                result.clipStart2 ? XO("start") : XO("end"),
                result.name2,
                result.index1 + 1,
                result.index2 + 1,
                nClips,
                longName
                );
        }

        if (message.empty()) {
            message = str;
        } else {
            message = XO("%s, %s").Format(message, str);
        }
    }

    return message;
}

void DoSelectClipBoundary(AudacityProject& project, bool next)
{
    auto& selectedRegion = ViewInfo::Get(project).selectedRegion;
    auto& trackFocus = TrackFocus::Get(project);

    std::vector<FoundClipBoundary> results;
    FindClipBoundaries(project, next ? selectedRegion.t1()
                       : selectedRegion.t0(), next, results);

    if (results.size() > 0) {
        // note that if there is more than one result, each has the same time
        // value.
        if (next) {
            selectedRegion.setT1(results[0].time);
        } else {
            selectedRegion.setT0(results[0].time);
        }

        ProjectHistory::Get(project).ModifyState(false);

        auto message = ClipBoundaryMessage(results);
        trackFocus.MessageForScreenReader(message);
    }
}

FoundClip FindNextClip(const WaveTrack* wt, double t0, double t1)
{
    FoundClip result{};
    result.waveTrack = wt;
    const auto clips = wt->SortedIntervalArray();

    t0 = AdjustForFindingStartTimes(clips, t0);

    {
        auto p = std::find_if(clips.begin(), clips.end(),
                              [&] (const auto& clip) {
            return clip->GetPlayStartTime() == t0;
        });
        if (p != clips.end() && (*p)->GetPlayEndTime() > t1) {
            result.found = true;
            result.startTime = (*p)->GetPlayStartTime();
            result.endTime = (*p)->GetPlayEndTime();
            result.name = (*p)->GetName();
            result.index = std::distance(clips.begin(), p);
            return result;
        }
    }

    {
        auto p = std::find_if(clips.begin(), clips.end(),
                              [&] (const auto& clip) {
            return clip->GetPlayStartTime() > t0;
        });
        if (p != clips.end()) {
            result.found = true;
            result.startTime = (*p)->GetPlayStartTime();
            result.endTime = (*p)->GetPlayEndTime();
            result.name = (*p)->GetName();
            result.index = std::distance(clips.begin(), p);
            return result;
        }
    }

    return result;
}

FoundClip FindPrevClip(const WaveTrack* wt, double t0, double t1)
{
    FoundClip result{};
    result.waveTrack = wt;
    const auto clips = wt->SortedIntervalArray();

    t0 = AdjustForFindingStartTimes(clips, t0);

    {
        auto p = std::find_if(clips.begin(), clips.end(),
                              [&] (const auto& clip) {
            return clip->GetPlayStartTime() == t0;
        });
        if (p != clips.end() && (*p)->GetPlayEndTime() < t1) {
            result.found = true;
            result.startTime = (*p)->GetPlayStartTime();
            result.endTime = (*p)->GetPlayEndTime();
            result.name = (*p)->GetName();
            result.index = std::distance(clips.begin(), p);
            return result;
        }
    }

    {
        auto p = std::find_if(clips.rbegin(), clips.rend(),
                              [&] (const auto& clip) {
            return clip->GetPlayStartTime() < t0;
        });
        if (p != clips.rend()) {
            result.found = true;
            result.startTime = (*p)->GetPlayStartTime();
            result.endTime = (*p)->GetPlayEndTime();
            result.name = (*p)->GetName();
            result.index
                =static_cast<int>(clips.size()) - 1
                  - std::distance(clips.rbegin(), p);
            return result;
        }
    }

    return result;
}

int FindClips
    (AudacityProject& project,
    double t0, double t1, bool next, std::vector<FoundClip>& finalResults)
{
    auto& tracks = TrackList::Get(project);
    finalResults.clear();

    bool anyWaveTracksSelected{ tracks.Selected<const WaveTrack>() };

    // first search the tracks individually

    std::vector<FoundClip> results;

    int nTracksSearched = 0;
    auto all = tracks.Any();
    auto waveTracks = all.Filter<const WaveTrack>();
    if (anyWaveTracksSelected) {
        waveTracks = waveTracks + &Track::GetSelected;
    }
    for (auto waveTrack : waveTracks) {
        auto result = next ? FindNextClip(waveTrack, t0, t1)
                      : FindPrevClip(waveTrack, t0, t1);
        if (result.found) {
            result.trackNum
                =1 + std::distance(all.begin(), all.find(waveTrack));
            results.push_back(result);
        }
        nTracksSearched++;
    }

    if (results.size() > 0) {
        // if any clips were found,
        // find the clip or clips with the min/max start time
        auto compareStart = [] (const FoundClip& a, const FoundClip& b)
        { return a.startTime < b.startTime; };

        auto pStart = next
                      ? std::min_element(results.begin(), results.end(), compareStart)
                      : std::max_element(results.begin(), results.end(), compareStart);

        std::vector<FoundClip> resultsStartTime;
        for ( auto& r : results ) {
            if (r.startTime == (*pStart).startTime) {
                resultsStartTime.push_back(r);
            }
        }

        if (resultsStartTime.size() > 1) {
            // more than one clip with same start time so
            // find the clip or clips with the min/max end time
            auto compareEnd = [] (const FoundClip& a, const FoundClip& b)
            { return a.endTime < b.endTime; };

            auto pEnd = next ? std::min_element(resultsStartTime.begin(),
                                                resultsStartTime.end(), compareEnd)
                        : std::max_element(resultsStartTime.begin(),
                                           resultsStartTime.end(), compareEnd);

            for ( auto& r : resultsStartTime ) {
                if (r.endTime == (*pEnd).endTime) {
                    finalResults.push_back(r);
                }
            }
        } else {
            finalResults = resultsStartTime;
        }
    }

    return nTracksSearched; // can be used for screen reader messages if required
}

void DoSelectClip(AudacityProject& project, bool next)
{
    auto& selectedRegion = ViewInfo::Get(project).selectedRegion;
    auto& trackFocus = TrackFocus::Get(project);
    auto& viewport = Viewport::Get(project);

    std::vector<FoundClip> results;
    FindClips(project, selectedRegion.t0(),
              selectedRegion.t1(), next, results);

    if (results.size() > 0) {
        // note that if there is more than one result, each has the same start
        // and end time
        double t0 = results[0].startTime;
        double t1 = results[0].endTime;
        selectedRegion.setTimes(t0, t1);
        ProjectHistory::Get(project).ModifyState(false);
        viewport.ScrollIntoView(selectedRegion.t0());

        // create and send message to screen reader
        TranslatableString message;
        for (auto& result : results) {
            auto longName = result.ComposeTrackName();
            auto nClips = result.waveTrack->GetNumClips();
            auto str = XP(
                /* i18n-hint:
                   first string is the name of a clip,
                   first number gives the position of that clip
                   in a sequence of clips,
                   last number counts all clips,
                   last string names a track */
                "%s, %d of %d clip %s",
                "%s, %d of %d clips %s",
                2
                )(
                result.name,
                result.index + 1,
                nClips,
                longName
                );

            if (message.empty()) {
                message = str;
            } else {
                message = XO("%s, %s").Format(message, str);
            }
        }
        trackFocus.MessageForScreenReader(message);
    }
}

void DoCursorClipBoundary
    (AudacityProject& project, bool next)
{
    auto& selectedRegion = ViewInfo::Get(project).selectedRegion;
    auto& trackFocus = TrackFocus::Get(project);
    auto& viewport = Viewport::Get(project);

    std::vector<FoundClipBoundary> results;
    FindClipBoundaries(project, next ? selectedRegion.t1()
                       : selectedRegion.t0(), next, results);

    if (results.size() > 0) {
        // note that if there is more than one result, each has the same time
        // value.
        double time = results[0].time;
        selectedRegion.setTimes(time, time);
        ProjectHistory::Get(project).ModifyState(false);
        viewport.ScrollIntoView(selectedRegion.t0());

        auto message = ClipBoundaryMessage(results);
        trackFocus.MessageForScreenReader(message);
    }
}

// This function returns the amount moved.  Possibly 0.0.
double DoClipMove(AudacityProject& project, TrackList& trackList,
                  bool syncLocked, bool right)
{
    auto& trackFocus = TrackFocus::Get(project);
    auto& viewInfo = ViewInfo::Get(project);
    auto& selectedRegion = viewInfo.selectedRegion;

    auto track = trackFocus.Get();
    if (track) {
        ClipMoveState state;

        auto t0 = selectedRegion.t0();

        std::unique_ptr<TrackShifter> uShifter;

        auto hitTestResult = TrackShifter::HitTestResult::Track;
        uShifter = MakeTrackShifter::Call(*track, project);
        if ((hitTestResult = uShifter->HitTest(t0, viewInfo))
            == TrackShifter::HitTestResult::Miss) {
            return 0.0;
        }

        auto pShifter = uShifter.get();
        auto desiredT0 = viewInfo.OffsetTimeByPixels(t0, (right ? 1 : -1));
        auto desiredSlideAmount = pShifter->HintOffsetLarger(desiredT0 - t0);

        state.Init(project, pShifter->GetTrack(), hitTestResult, move(uShifter),
                   t0, viewInfo, trackList, syncLocked);

        auto hSlideAmount = state.DoSlideHorizontal(desiredSlideAmount);

        double newT0 = t0 + hSlideAmount;
        if (hitTestResult != TrackShifter::HitTestResult::Track) {
            // If necessary, correct for rounding errors. For example,
            // for a wavetrack, ensure that t0 is still in the clip
            // which it was within before the move.
            // (pShifter is still undestroyed in the ClipMoveState.)
            newT0 = pShifter->AdjustT0(newT0);
        }

        double diff = selectedRegion.duration();
        selectedRegion.setTimes(newT0, newT0 + diff);

        return hSlideAmount;
    }
    return 0.0;
}

void DoClipLeftOrRight
    (AudacityProject& project, bool right, bool keyUp)
{
    auto& undoManager = UndoManager::Get(project);
    auto& viewport = Viewport::Get(project);

    if (keyUp) {
        undoManager.StopConsolidating();
        return;
    }

    auto& trackFocus = TrackFocus::Get(project);
    auto& viewInfo = ViewInfo::Get(project);
    auto& selectedRegion = viewInfo.selectedRegion;
    auto& tracks = TrackList::Get(project);
    auto isSyncLocked = SyncLockState::Get(project).IsSyncLocked();

    auto amount = DoClipMove(project, tracks, isSyncLocked, right);

    viewport.ScrollIntoView(selectedRegion.t0());

    if (amount != 0.0) {
        auto message = right ? XO("Moved clips to the right")
                       : XO("Moved clips to the left");

        // The following use of the UndoPush flags is so that both a single
        // keypress (keydown, then keyup), and holding down a key
        // (multiple keydowns followed by a keyup) result in a single
        // entry in Audacity's history dialog.
        ProjectHistory::Get(project)
        .PushState(message, XO("Move audio clips"), UndoPush::CONSOLIDATE);
    }

    if (amount == 0.0) {
        trackFocus.MessageForScreenReader(XO("clip not moved"));
    }
}
}

/// Namespace for functions for Clip menu
namespace {
// exported helper functions
// none

// Menu handler functions

void OnSelectPrevClipBoundaryToCursor
    (const CommandContext& context)
{
    auto& project = context.project;
    DoSelectClipBoundary(project, false);
}

void OnSelectCursorToNextClipBoundary
    (const CommandContext& context)
{
    auto& project = context.project;
    DoSelectClipBoundary(project, true);
}

void OnSelectPrevClip(const CommandContext& context)
{
    auto& project = context.project;
    DoSelectClip(project, false);
}

void OnSelectNextClip(const CommandContext& context)
{
    auto& project = context.project;
    DoSelectClip(project, true);
}

void OnCursorPrevClipBoundary(const CommandContext& context)
{
    AudacityProject& project = context.project;

    DoCursorClipBoundary(project, false);
}

void OnCursorNextClipBoundary(const CommandContext& context)
{
    AudacityProject& project = context.project;

    DoCursorClipBoundary(project, true);
}

// PRL: Clip moving functions -- more than just selection adjustment.  Do they
// really belong in these navigation menus?
void OnClipLeft(const CommandContext& context)
{
    auto& project = context.project;
    auto evt = context.pEvt;
    if (evt) {
        DoClipLeftOrRight(project, false, evt->GetEventType() == wxEVT_KEY_UP);
    } else {            // called from menu, so simulate keydown and keyup
        DoClipLeftOrRight(project, false, false);
        DoClipLeftOrRight(project, false, true);
    }
}

void OnClipRight(const CommandContext& context)
{
    auto& project = context.project;
    auto evt = context.pEvt;
    if (evt) {
        DoClipLeftOrRight(project, true, evt->GetEventType() == wxEVT_KEY_UP);
    } else {            // called from menu, so simulate keydown and keyup
        DoClipLeftOrRight(project, true, false);
        DoClipLeftOrRight(project, true, true);
    }
}

// Menu definitions

using namespace MenuRegistry;

// Register menu items

auto ClipSelectMenu()
{
    static auto menu = std::shared_ptr{
        Menu(wxT("Clip"), XXO("Audi&o Clips"),
             Command(wxT("SelPrevClipBoundaryToCursor"),
                     XXO("Pre&vious Clip Boundary to Cursor"),
                     OnSelectPrevClipBoundaryToCursor,
                     WaveTracksExistFlag()),
             Command(wxT("SelCursorToNextClipBoundary"),
                     XXO("Cursor to Ne&xt Clip Boundary"),
                     OnSelectCursorToNextClipBoundary,
                     WaveTracksExistFlag()),
             Command(wxT("SelPrevClip"), XXO("Previo&us Clip"),
                     OnSelectPrevClip, WaveTracksExistFlag(),
                     Options { wxT("Alt+,"), XO("Select Previous Clip") }),
             Command(wxT("SelNextClip"), XXO("N&ext Clip"), OnSelectNextClip,
                     WaveTracksExistFlag(),
                     Options { wxT("Alt+."), XO("Select Next Clip") })
             ) };
    return menu;
}

AttachedItem sAttachment1{ Indirect(ClipSelectMenu()), wxT("Select/Basic") };

auto ClipCursorItems()
{
    static auto items = std::shared_ptr{
        Items(wxT("Clip"),
              Command(wxT("CursPrevClipBoundary"), XXO("Pre&vious Clip Boundary"),
                      OnCursorPrevClipBoundary,
                      WaveTracksExistFlag(),
                      Options {}.LongName(XO("Cursor to Prev Clip Boundary"))),
              Command(wxT("CursNextClipBoundary"), XXO("Ne&xt Clip Boundary"),
                      OnCursorNextClipBoundary,
                      WaveTracksExistFlag(),
                      Options {}.LongName(XO("Cursor to Next Clip Boundary")))
              ) };
    return items;
}

AttachedItem sAttachment2{ Indirect(ClipCursorItems()),
                           { wxT("Transport/Basic/Cursor"),
                             { OrderingHint::Before, wxT("CursProjectStart") } }
};

auto ExtraTimeShiftItems()
{
    static auto items = std::shared_ptr{
        Items(wxT("TimeShift"),
              Command(wxT("ClipLeft"), XXO("Time Shift &Left"), OnClipLeft,
                      TracksExistFlag() | TrackPanelHasFocus(), Options {}.WantKeyUp()),
              Command(wxT("ClipRight"), XXO("Time Shift &Right"), OnClipRight,
                      TracksExistFlag() | TrackPanelHasFocus(), Options {}.WantKeyUp())
              ) };
    return items;
}

AttachedItem sAttachment3{ Indirect(ExtraTimeShiftItems()),
                           { wxT("Optional/Extra/Part1/Edit"), { OrderingHint::End, {} } }
};
}
